<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Pdf
 * @subpackage Fonts
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id: Cmap.php 18993 2009-11-15 17:09:16Z alexander $
 */


/**
 * Abstract helper class for {@link Zend_Pdf_Resource_Font} which manages font
 * character maps.
 *
 * Defines the public interface for concrete subclasses which are responsible
 * for mapping Unicode characters to the font's glyph numbers. Also provides
 * shared utility methods.
 *
 * Cmap objects should ordinarily be obtained through the factory method
 * {@link cmapWithTypeData()}.
 *
 * The supported character map types are those found in the OpenType spec. For
 * additional detail on the internal binary format of these tables, see:
 * <ul>
 *  <li>{@link http://developer.apple.com/textfonts/TTRefMan/RM06/Chap6cmap.html}
 *  <li>{@link http://www.microsoft.com/OpenType/OTSpec/cmap.htm}
 *  <li>{@link http://partners.adobe.com/public/developer/opentype/index_cmap.html}
 * </ul>
 *
 * @todo Write code for Zend_Pdf_FontCmap_HighByteMapping class.
 * @todo Write code for Zend_Pdf_FontCmap_MixedCoverage class.
 * @todo Write code for Zend_Pdf_FontCmap_TrimmedArray class.
 * @todo Write code for Zend_Pdf_FontCmap_SegmentedCoverage class.
 *
 * @package    Zend_Pdf
 * @subpackage Fonts
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
abstract class Zend_Pdf_Cmap
{
	/**** Class Constants ****/


	/* Cmap Table Types */

	/**
	 * Byte Encoding character map table type.
	 */
	const TYPE_BYTE_ENCODING = 0x00;

	/**
	 * High Byte Mapping character map table type.
	 */
	const TYPE_HIGH_BYTE_MAPPING = 0x02;

	/**
	 * Segment Value to Delta Mapping character map table type.
	 */
	const TYPE_SEGMENT_TO_DELTA = 0x04;

	/**
	 * Trimmed Table character map table type.
	 */
	const TYPE_TRIMMED_TABLE = 0x06;

	/**
	 * Mixed Coverage character map table type.
	 */
	const TYPE_MIXED_COVERAGE = 0x08;

	/**
	 * Trimmed Array character map table type.
	 */
	const TYPE_TRIMMED_ARRAY = 0x0a;

	/**
	 * Segmented Coverage character map table type.
	 */
	const TYPE_SEGMENTED_COVERAGE = 0x0c;

	/**
	 * Static Byte Encoding character map table type. Variant of
	 * {@link TYPE_BYTEENCODING}.
	 */
	const TYPE_BYTE_ENCODING_STATIC = 0xf1;

	/**
	 * Unknown character map table type.
	 */
	const TYPE_UNKNOWN = 0xff;


	/* Special Glyph Names */

	/**
	 * Glyph representing missing characters.
	 */
	const MISSING_CHARACTER_GLYPH = 0x00;



	/**** Public Interface ****/


	/* Factory Methods */

	/**
	 * Instantiates the appropriate concrete subclass based on the type of cmap
	 * table and returns the instance.
	 *
	 * The cmap type must be one of the following values:
	 * <ul>
	 *  <li>{@link Zend_Pdf_Cmap::TYPE_BYTE_ENCODING}
	 *  <li>{@link Zend_Pdf_Cmap::TYPE_BYTE_ENCODING_STATIC}
	 *  <li>{@link Zend_Pdf_Cmap::TYPE_HIGH_BYTE_MAPPING}
	 *  <li>{@link Zend_Pdf_Cmap::TYPE_SEGMENT_TO_DELTA}
	 *  <li>{@link Zend_Pdf_Cmap::TYPE_TRIMMED_TABLE}
	 *  <li>{@link Zend_Pdf_Cmap::TYPE_MIXED_COVERAGE}
	 *  <li>{@link Zend_Pdf_Cmap::TYPE_TRIMMED_ARRAY}
	 *  <li>{@link Zend_Pdf_Cmap::TYPE_SEGMENTED_COVERAGE}
	 * </ul>
	 *
	 * Throws an exception if the table type is invalid or the cmap table data
	 * cannot be validated.
	 *
	 * @param integer $cmapType Type of cmap.
	 * @param mixed $cmapData Cmap table data. Usually a string or array.
	 * @return Zend_Pdf_Cmap
	 * @throws Zend_Pdf_Exception
	 */
	public static function cmapWithTypeData($cmapType, $cmapData)
	{
		switch ($cmapType) {
			case Zend_Pdf_Cmap::TYPE_BYTE_ENCODING:
				require_once 'Zend/Pdf/Cmap/ByteEncoding.php';
				return new Zend_Pdf_Cmap_ByteEncoding($cmapData);

			case Zend_Pdf_Cmap::TYPE_BYTE_ENCODING_STATIC:
				require_once 'Zend/Pdf/Cmap/ByteEncoding/Static.php';
				return new Zend_Pdf_Cmap_ByteEncoding_Static($cmapData);

			case Zend_Pdf_Cmap::TYPE_HIGH_BYTE_MAPPING:
				require_once 'Zend/Pdf/Exception.php';
				throw new Zend_Pdf_Exception('High byte mapping cmap currently unsupported',
				Zend_Pdf_Exception::CMAP_TYPE_UNSUPPORTED);

			case Zend_Pdf_Cmap::TYPE_SEGMENT_TO_DELTA:
				require_once 'Zend/Pdf/Cmap/SegmentToDelta.php';
				return new Zend_Pdf_Cmap_SegmentToDelta($cmapData);

			case Zend_Pdf_Cmap::TYPE_TRIMMED_TABLE:
				require_once 'Zend/Pdf/Cmap/TrimmedTable.php';
				return new Zend_Pdf_Cmap_TrimmedTable($cmapData);

			case Zend_Pdf_Cmap::TYPE_MIXED_COVERAGE:
				require_once 'Zend/Pdf/Exception.php';
				throw new Zend_Pdf_Exception('Mixed coverage cmap currently unsupported',
				Zend_Pdf_Exception::CMAP_TYPE_UNSUPPORTED);

			case Zend_Pdf_Cmap::TYPE_TRIMMED_ARRAY:
				require_once 'Zend/Pdf/Exception.php';
				throw new Zend_Pdf_Exception('Trimmed array cmap currently unsupported',
				Zend_Pdf_Exception::CMAP_TYPE_UNSUPPORTED);

			case Zend_Pdf_Cmap::TYPE_SEGMENTED_COVERAGE:
				require_once 'Zend/Pdf/Exception.php';
				throw new Zend_Pdf_Exception('Segmented coverage cmap currently unsupported',
				Zend_Pdf_Exception::CMAP_TYPE_UNSUPPORTED);

			default:
				require_once 'Zend/Pdf/Exception.php';
				throw new Zend_Pdf_Exception("Unknown cmap type: $cmapType",
				Zend_Pdf_Exception::CMAP_UNKNOWN_TYPE);
		}
	}


	/* Abstract Methods */

	/**
	 * Object constructor
	 *
	 * Parses the raw binary table data. Throws an exception if the table is
	 * malformed.
	 *
	 * @param string $cmapData Raw binary cmap table data.
	 * @throws Zend_Pdf_Exception
	 */
	abstract public function __construct($cmapData);

	/**
	 * Returns an array of glyph numbers corresponding to the Unicode characters.
	 *
	 * If a particular character doesn't exist in this font, the special 'missing
	 * character glyph' will be substituted.
	 *
	 * See also {@link glyphNumberForCharacter()}.
	 *
	 * @param array $characterCodes Array of Unicode character codes (code points).
	 * @return array Array of glyph numbers.
	 */
	abstract public function glyphNumbersForCharacters($characterCodes);

	/**
	 * Returns the glyph number corresponding to the Unicode character.
	 *
	 * If a particular character doesn't exist in this font, the special 'missing
	 * character glyph' will be substituted.
	 *
	 * See also {@link glyphNumbersForCharacters()} which is optimized for bulk
	 * operations.
	 *
	 * @param integer $characterCode Unicode character code (code point).
	 * @return integer Glyph number.
	 */
	abstract public function glyphNumberForCharacter($characterCode);

	/**
	 * Returns an array containing the Unicode characters that have entries in
	 * this character map.
	 *
	 * @return array Unicode character codes.
	 */
	abstract public function getCoveredCharacters();

	/**
	 * Returns an array containing the glyphs numbers that have entries in this character map.
	 * Keys are Unicode character codes (integers)
	 *
	 * This functionality is partially covered by glyphNumbersForCharacters(getCoveredCharacters())
	 * call, but this method do it in more effective way (prepare complete list instead of searching
	 * glyph for each character code).
	 *
	 * @internal
	 * @return array Array representing <Unicode character code> => <glyph number> pairs.
	 */
	abstract public function getCoveredCharactersGlyphs();


	/**** Internal Methods ****/


	/* Internal Utility Methods */

	/**
	 * Extracts a signed 2-byte integer from a string.
	 *
	 * Integers are always big-endian. Throws an exception if the index is out
	 * of range.
	 *
	 * @param string &$data
	 * @param integer $index Position in string of integer.
	 * @return integer
	 * @throws Zend_Pdf_Exception
	 */
	protected function _extractInt2(&$data, $index)
	{
		if (($index < 0) | (($index + 1) > strlen($data))) {
			require_once 'Zend/Pdf/Exception.php';
			throw new Zend_Pdf_Exception("Index out of range: $index",
			Zend_Pdf_Exception::INDEX_OUT_OF_RANGE);
		}
		$number = ord($data[$index]);
		if (($number & 0x80) == 0x80) {    // negative
			$number = ~((((~ $number) & 0xff) << 8) | ((~ ord($data[++$index])) & 0xff));
		} else {
			$number = ($number << 8) | ord($data[++$index]);
		}
		return $number;
	}

	/**
	 * Extracts an unsigned 2-byte integer from a string.
	 *
	 * Integers are always big-endian. Throws an exception if the index is out
	 * of range.
	 *
	 * @param string &$data
	 * @param integer $index Position in string of integer.
	 * @return integer
	 * @throws Zend_Pdf_Exception
	 */
	protected function _extractUInt2(&$data, $index)
	{
		if (($index < 0) | (($index + 1) > strlen($data))) {
			require_once 'Zend/Pdf/Exception.php';
			throw new Zend_Pdf_Exception("Index out of range: $index",
			Zend_Pdf_Exception::INDEX_OUT_OF_RANGE);
		}
		$number = (ord($data[$index]) << 8) | ord($data[++$index]);
		return $number;
	}

	/**
	 * Extracts an unsigned 4-byte integer from a string.
	 *
	 * Integers are always big-endian. Throws an exception if the index is out
	 * of range.
	 *
	 * NOTE: If you ask for a 4-byte unsigned integer on a 32-bit machine, the
	 * resulting value WILL BE SIGNED because PHP uses signed integers internally
	 * for everything. To guarantee portability, be sure to use bitwise or
	 * similar operators on large integers!
	 *
	 * @param string &$data
	 * @param integer $index Position in string of integer.
	 * @return integer
	 * @throws Zend_Pdf_Exception
	 */
	protected function _extractUInt4(&$data, $index)
	{
		if (($index < 0) | (($index + 3) > strlen($data))) {
			require_once 'Zend/Pdf/Exception.php';
			throw new Zend_Pdf_Exception("Index out of range: $index",
			Zend_Pdf_Exception::INDEX_OUT_OF_RANGE);
		}
		$number = (ord($data[$index]) << 24) | (ord($data[++$index]) << 16) |
		(ord($data[++$index]) << 8) | ord($data[++$index]);
		return $number;
	}

}
